package cmd

import (
	"errors"
	"fmt"
	"net/http"
	_ "net/http/pprof" // pprof handler
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/evcc-io/evcc/core"
	"github.com/evcc-io/evcc/core/keys"
	"github.com/evcc-io/evcc/push"
	"github.com/evcc-io/evcc/server"
	"github.com/evcc-io/evcc/server/updater"
	"github.com/evcc-io/evcc/util"
	"github.com/evcc-io/evcc/util/auth"
	"github.com/evcc-io/evcc/util/config"
	"github.com/evcc-io/evcc/util/pipe"
	"github.com/evcc-io/evcc/util/sponsor"
	"github.com/evcc-io/evcc/util/telemetry"
	_ "github.com/joho/godotenv/autoload"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/spf13/cast"
	"github.com/spf13/cobra"
	vpr "github.com/spf13/viper"
)

const (
	rebootDelay = 15 * time.Minute // delayed reboot on error
	serviceDB   = "/var/lib/evcc/evcc.db"
	userDB      = "~/.evcc/evcc.db"
)

var (
	log         = util.NewLogger("main")
	cfgFile     string
	cfgDatabase string

	ignoreEmpty = ""                                      // ignore empty keys
	ignoreLogs  = []string{"log"}                         // ignore log messages, including warn/error
	ignoreMqtt  = []string{"log", "auth", "releaseNotes"} // excessive size may crash certain brokers

	viper *vpr.Viper

	runAsService bool
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:     "evcc",
	Short:   "evcc - open source solar charging",
	Version: server.FormattedVersion(),
	Run:     runRoot,
}

func init() {
	viper = vpr.NewWithOptions(vpr.ExperimentalBindStruct())

	cobra.OnInitialize(initConfig)

	// global options
	rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "Config file (default \"~/evcc.yaml\" or \"/etc/evcc.yaml\")")
	rootCmd.PersistentFlags().StringVar(&cfgDatabase, "database", "", "Database location (default \"~/.evcc/evcc.db\")")
	rootCmd.PersistentFlags().BoolP("help", "h", false, "Help")
	rootCmd.PersistentFlags().Bool(flagHeaders, false, flagHeadersDescription)
	rootCmd.PersistentFlags().Bool(flagIgnoreDatabase, false, flagIgnoreDatabaseDescription)

	// config file options
	rootCmd.PersistentFlags().StringP("log", "l", "info", "Log level (fatal, error, warn, info, debug, trace)")
	bindP(rootCmd, "log")

	rootCmd.Flags().Bool("metrics", false, "Expose metrics")
	bind(rootCmd, "metrics")

	rootCmd.Flags().Bool("profile", false, "Expose pprof profiles")
	bind(rootCmd, "profile")

	rootCmd.Flags().Bool(flagDisableAuth, false, flagDisableAuthDescription)
}

// initConfig reads in config file and ENV variables if set
func initConfig() {
	if cfgFile != "" {
		// Use config file from the flag
		viper.SetConfigFile(cfgFile)
	} else {
		// Search for config in home directory if available
		if home, err := os.UserHomeDir(); err == nil {
			viper.AddConfigPath(home)
		}

		// Search config in home directory with name "mbmd" (without extension).
		viper.AddConfigPath(".")    // optionally look for config in the working directory
		viper.AddConfigPath("/etc") // path to look for the config file in

		viper.SetConfigName("evcc")
	}
	if cfgDatabase != "" {
		viper.Set("Database.Dsn", cfgDatabase)
	}

	viper.SetEnvPrefix("evcc")
	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
	viper.AutomaticEnv() // read in environment variables that match

	// print version
	util.LogLevel("info", nil)
	log.INFO.Printf("evcc %s", server.FormattedVersion())
}

// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func runRoot(cmd *cobra.Command, args []string) {
	runAsService = true

	// load config and re-configure logging after reading config file
	var err error
	if cfgErr := loadConfigFile(&conf, !cmd.Flag(flagIgnoreDatabase).Changed); errors.As(cfgErr, &vpr.ConfigFileNotFoundError{}) {
		log.INFO.Println("missing config file - switching into demo mode")
		if err := demoConfig(&conf); err != nil {
			log.FATAL.Fatal(err)
		}
	} else {
		err = wrapErrorWithClass(ClassConfigFile, cfgErr)
	}

	// setup environment
	if err == nil {
		err = configureEnvironment(cmd, &conf)
	}

	// configure network
	if err == nil {
		err = networkSettings(&conf.Network)
	}

	log.INFO.Printf("listening at :%d", conf.Network.Port)

	// start broadcasting values
	tee := new(util.Tee)
	valueChan := make(chan util.Param, 64)
	go tee.Run(valueChan)

	// value cache
	cache := util.NewParamCache()
	go cache.Run(pipe.NewDropper(ignoreLogs...).Pipe(tee.Attach()))

	// create web server
	socketHub := server.NewSocketHub()
	httpd := server.NewHTTPd(fmt.Sprintf(":%d", conf.Network.Port), socketHub)

	// metrics
	if viper.GetBool("metrics") {
		httpd.Router().Handle("/metrics", promhttp.Handler())
	}

	// pprof
	if viper.GetBool("profile") {
		httpd.Router().PathPrefix("/debug/").Handler(http.DefaultServeMux)
	}

	// publish to UI
	go socketHub.Run(pipe.NewDropper(ignoreEmpty).Pipe(tee.Attach()), cache)

	// capture log messages for UI
	util.CaptureLogs(valueChan)

	// setup telemetry
	if err == nil {
		telemetry.Create(conf.Plant)
		if conf.Telemetry {
			err = telemetry.Enable(true)
		}
	}

	// setup modbus proxy
	if err == nil {
		err = wrapErrorWithClass(ClassModbusProxy, configureModbusProxy(&conf.ModbusProxy))
	}

	// setup site and loadpoints
	var site *core.Site
	if err == nil {
		site, err = configureSiteAndLoadpoints(&conf)
	}

	// setup influx
	if err == nil {
		influx, ierr := configureInflux(&conf.Influx)
		if ierr != nil {
			err = wrapErrorWithClass(ClassInflux, ierr)
		}

		if err == nil && influx != nil {
			// eliminate duplicate values
			dedupe := pipe.NewDeduplicator(30*time.Minute,
				keys.VehicleSoc, keys.VehicleRange, keys.VehicleOdometer,
				keys.TariffCo2, keys.TariffFeedIn, keys.TariffGrid,
				keys.ChargedEnergy, keys.ChargeRemainingEnergy)
			go influx.Run(site, dedupe.Pipe(
				pipe.NewDropper(append(ignoreLogs, ignoreEmpty, keys.Forecast)...).Pipe(tee.Attach()),
			))
		}
	}

	// remove previous fatal startup errors
	valueChan <- util.Param{Key: keys.Fatal, Val: nil}

	// setup mqtt publisher
	if err == nil && conf.Mqtt.Broker != "" {
		var mqtt *server.MQTT
		mqtt, err = server.NewMQTT(strings.Trim(conf.Mqtt.Topic, "/"), site)
		if err == nil {
			go mqtt.Run(site, pipe.NewDropper(append(ignoreMqtt, ignoreEmpty)...).Pipe(tee.Attach()))
		}
	}

	// announce on mDNS
	if err == nil && strings.HasSuffix(conf.Network.Host, ".local") {
		err = configureMDNS(conf.Network)
	}

	// start HEMS server
	if err == nil {
		err = wrapErrorWithClass(ClassHEMS, configureHEMS(&conf.HEMS, site, httpd))
	}

	// setup messaging
	var pushChan chan push.Event
	if err == nil {
		pushChan, err = configureMessengers(&conf.Messaging, site.Vehicles(), valueChan, cache)
		err = wrapErrorWithClass(ClassMessenger, err)
	}

	// publish initial settings
	valueChan <- util.Param{Key: keys.EEBus, Val: conf.EEBus.Configured()}
	valueChan <- util.Param{Key: keys.Hems, Val: conf.HEMS}
	valueChan <- util.Param{Key: keys.Influx, Val: conf.Influx}
	valueChan <- util.Param{Key: keys.Interval, Val: conf.Interval}
	valueChan <- util.Param{Key: keys.Messaging, Val: pushChan != nil}
	valueChan <- util.Param{Key: keys.ModbusProxy, Val: conf.ModbusProxy}
	valueChan <- util.Param{Key: keys.Mqtt, Val: conf.Mqtt}
	valueChan <- util.Param{Key: keys.Network, Val: conf.Network}
	valueChan <- util.Param{Key: keys.Sponsor, Val: sponsor.Status()}

	// run shutdown functions on stop
	var once sync.Once
	stopC := make(chan struct{})

	// catch signals
	go func() {
		signalC := make(chan os.Signal, 1)
		signal.Notify(signalC, os.Interrupt, syscall.SIGTERM)

		<-signalC                        // wait for signal
		once.Do(func() { close(stopC) }) // signal loop to end
	}()

	// wait for shutdown
	go func() {
		<-stopC

		select {
		case <-shutdownDoneC(): // wait for shutdown
		case <-time.After(conf.Interval):
		}

		// exit code 1 on error
		os.Exit(cast.ToInt(err != nil))
	}()

	// allow web access for vehicles
	configureAuth(conf.Network, config.Instances(config.Vehicles().Devices()), httpd.Router(), valueChan)

	auth := auth.New()
	if ok, _ := cmd.Flags().GetBool(flagDisableAuth); ok {
		log.WARN.Println("❗❗❗ Authentication is disabled. This is dangerous. Your data and credentials are not protected.")
		auth.Disable()
	}

	httpd.RegisterSystemHandler(valueChan, cache, auth, func() {
		log.INFO.Println("evcc was stopped by user. OS should restart the service. Or restart manually.")
		err = errors.New("restart required") // https://gokrazy.org/development/process-interface/
		once.Do(func() { close(stopC) })     // signal loop to end
	})

	// show and check version, reduce api load during development
	if server.Version != server.DevVersion {
		valueChan <- util.Param{Key: keys.Version, Val: server.FormattedVersion()}
		go updater.Run(log, httpd, valueChan)
	}

	// setup site
	if err == nil {
		// set channels
		site.DumpConfig()
		site.Prepare(valueChan, pushChan)

		httpd.RegisterSiteHandlers(site, valueChan)

		go func() {
			site.Run(stopC, conf.Interval)
		}()
	}

	if err != nil {
		// improve error message
		err = wrapFatalError(err)
		valueChan <- util.Param{Key: keys.Fatal, Val: err}

		// TODO stop reboot loop if user updates config (or show countdown in UI)
		log.FATAL.Println(err)
		log.FATAL.Printf("will attempt restart in: %v", rebootDelay)

		go func() {
			<-time.After(rebootDelay)
			once.Do(func() { close(stopC) }) // signal loop to end
		}()
	}

	// uds health check listener
	go server.HealthListener(site)

	log.FATAL.Println(wrapFatalError(httpd.ListenAndServe()))
}
